﻿using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestListGenerator
{
    /// <summary>
    /// This class is used to generate TestLists. You pass in a populated
    /// Arguments object (detailing the DLL to query for the tests, the 
    /// included and excluded categories and output paths) and it does 
    /// the rest.
    /// </summary>
    public class TestListGenerator
    {
        /// <summary>
        /// The configuration that is used to generate the test list
        /// </summary>
        public Configuration Configuration
        {
            get;
            set;
        }

        /// <summary>
        /// Validates the configuration to make sure it has the right mix of parameters
        /// At this stage, all we need is a DLL Path and either IncludedCategories, or
        /// ExcludedCategories, or both
        /// </summary>
        /// <returns>Returns a string explaining why the config is invalid. If it's valid, it returns null</returns>
        protected string ValidateConfiguration()
        {
            if (this.Configuration.DllPath == null) 
            {
                return "You need to provide a DLL Path";
            }
            if (this.Configuration.IncludedCategories.Count + this.Configuration.ExcludedCategories.Count == 0) 
            {
                return "You need to provide at least one category to Include or Exclude";
            }
            return null;
        }

        /// <summary>
        /// Create an object that creates TestLists based on the configuration 
        /// passed in.
        /// </summary>
        /// <param name="config">The configuration to use when generating the TestLists</param>
        public TestListGenerator(Configuration _configuration)
        {
            // Set the config
            this.Configuration = _configuration;

            // Validate the configuration
            string validationError = ValidateConfiguration();
            if (validationError != null) 
            {
                throw new TestListGeneratorException(TestListGeneratorExceptionType.MissingConfigurationParameter, validationError);
            } 
        }

        /// <summary>
        /// Checks to see if the test matches the criteria or not. NB: all 
        /// categories are changed to lower case
        /// </summary>
        /// <param name="args">The criteria used to do the matching</param>
        /// <param name="testCategories">The categories the test belongs to</param>
        /// <returns>True if the test matches the criteria, false otherwise</returns>
        protected bool IsIncluded(HashSet<string> testCategories)
        {
            // Cycle through the excluded categories, if the test matches, then
            // it's not included. 
            foreach (string category in this.Configuration.ExcludedCategories)
            {
                if (testCategories.Contains(category.ToLower()))
                {
                    return false;
                }
            }

            // Cycle through the included categories, if the test matches, then
            // it's included, as it wasn't in the excluded list above
            foreach (string category in this.Configuration.IncludedCategories)
            {
                if (testCategories.Contains(category.ToLower()))
                {
                    return true;
                }
            }

            // It wasn't in the excluded or included list, so it shouldn't be
            // included, unless the inclusion list was empty (in which case, it 
            // should be included)
            return (this.Configuration.IncludedCategories.Count == 0);
        }

        /// <summary>
        /// Generates a TestList based on Included / Excluded categories
        /// You need to provide the path to the DLL and the categories to 
        /// include or exclude 
        /// </summary>
        public void GenerateTestListBasedOnCategories()
        {
            // Check if the DLL exists
            if (File.Exists(this.Configuration.DllPath))
            {
                throw new TestListGeneratorException(TestListGeneratorExceptionType.TestDLLMissing, "Unable to find path to the Test DLL: " + this.Configuration.DllPath);
            }

            // Load in the DLL and grab all the tests and their categories
            Dictionary<MethodInfo, HashSet<string>> testCategoryMapping;
            try
            {
                testCategoryMapping =
                    ReflectionHelper.GetTestsFromAssembly(this.Configuration.DllPath);
            }
            catch (BadImageFormatException)
            {
                throw new TestListGeneratorException(TestListGeneratorExceptionType.TestDLLInvalidFormat, "The Test DLL passed in was not a valid .net assembly: " + this.Configuration.DllPath);
            }

            // Parse the categories to sort the tests into two lists,
            // included and excluded
            HashSet<MethodInfo> includedTests = new HashSet<MethodInfo>();
            HashSet<MethodInfo> excludedTests = new HashSet<MethodInfo>();

            foreach (MethodInfo mi in testCategoryMapping.Keys)
            {
                // Check if the test matches the criteria or not, and put
                // it in the appropriate list
                if (IsIncluded(testCategoryMapping[mi]))
                {
                    includedTests.Add(mi);
                }
                else
                {
                    excludedTests.Add(mi);
                }
            }

            // Create the TestLists
            TestListsTestList includedTestList = VsmdiHelper.CreateTestList(this.Configuration.DllPath, VsmdiHelper.VsmdiStaticMapKey.IncludedTestsTestList, null, includedTests);
            TestListsTestList excludedTestList = VsmdiHelper.CreateTestList(this.Configuration.DllPath, VsmdiHelper.VsmdiStaticMapKey.ExcludedTestsTestList, null, excludedTests);

            TestListsTestList[] createdTestLists = new TestListsTestList[] { includedTestList, excludedTestList };

            // Now combine them to create the file
            VsmdiHelper.CreateVsmdiConfigFile(this.Configuration.OutputPath, createdTestLists);
        }

        /// <summary>
        /// Returns the string that needs to be passed to MSTest
        /// to run the TestList created by this generator
        /// </summary>
        /// <param name="isIncluded">Should we run the included tests, or the excluded ones?</param>
        /// <returns>The parameters needed to be passed to MSTest to run the config</returns>
        public string MSTestParameters(bool isIncluded)
        {
            return string.Format("/testmetadata:{0} /runconfig:{1} /testlist:{2}", 
                this.Configuration.OutputPath, 
                this.Configuration.RunConfigPath,
                (isIncluded) ? "Included" : "Excluded"
            );
        }
    }
}
